package org.mock4as
{
	public class Mock  
	{
		public function Mock(){
		}  
		
		private var testFailed:Boolean = false;
		private var reason:String;
		private var _isLiberal:Boolean = false;
		private var expectedMethods:Array = new Array();
		// currentMethod is the method used while setting up the expectations
		// currentMethod changes every time we call expects(methodName)
		private var currentMethod:MethodInvocation;
		private var currentReturnValue:Object;
		private var currentException:Object;
		private var hasBeenVerified:Boolean = false;
		
		
		public function isLiberal():Mock
		{
			_isLiberal = true;
			return this;
		}
		
		/**
		 * Tells the mock to expect a method to be called.
		 *  
		 * @param methodName	Name of the method that is expected to be called.
		 * @return 				Returns the mock object. This makes it easy to string several
		 * 						method calls together. 
		 * 						Example: mock.expects("methodA").withArg(someParameter, anotherParam);
		 * 								 mock.methodA(someParameter, anotherParam);
		 * 								 asserTrue(mock.errorMessage(), mock.sucess());
		 * 
		 */		
		public function expects(methodName:String):Mock
		{
			currentMethod = new MethodInvocation(methodName)
			expectedMethods.push(currentMethod);
			return this;
		}

		/**
		 * Specifies the number of calls to a particular expected method. By default, a method will be expected to
		 * be called once (via the expects() method). Use the times() method to tell the mock to expect the method multiple
		 * times. Example: mock.expects("methodA").time(5);
		 * 
		 * @param timesInvoked	The number of times a method is expected to be called.
		 * @return 				Returns the mock object. This makes it easy to string several
		 * 						method calls together. 
		 * 
		 */		
		public function times(timesInvoked:uint):Mock
		{
			// If we don't expect the method to be called,
			// remove it from the expected methods array
			if (timesInvoked==0) expectedMethods.pop();
			currentMethod.timesInvoked = timesInvoked;
			return this;
		}
		
		/**
		 * Specifies the arguments that are expected to be passed to a particular method.
		 * Example: mock.expects("methodA").withArgs(arg1, arg2, arg3);
		 *  
		 * @param args	An array of arguments that are expected to be passed to a method.
		 * @return 		Returns the mock object. This makes it easy to string several
		 * 						method calls together. 
		 * 
		 */		
		public function withArgs(...args):Mock
		{
			currentMethod.args = args;
			return this;
		}
		
		/**
		 * Specifies the arguments that are expected to be passed to a particular method.
		 * Example: mock.expects("methodA").withArgs(arg1, arg2, arg3);
		 *  
		 * @param args	An array of arguments that are expected to be passed to a method.
		 * @return 		Returns the mock object. This makes it easy to string several
		 * 						method calls together. 
		 * 
		 */		
		public function withArg(arg:Object):Mock
		{
			currentMethod.args[0] = arg;
			return this;
		}
		
		/**
		 * Tells the mock to disregard the arguments passed to the mock. It will validate 
		 * that a method has been called, regardless of the parameters sent to it 
		 * 
		 * @return 		Returns the mock object. This makes it easy to string several
		 * 				method calls together. 
		 * 
		 * 
		 */		
		public function withAnyArgs():Mock
		{
			currentMethod.expectAnyArgs();
			return this;
		}
		
		/**
		 * Tells the mock that it should not expect any arguments to be passed to a method.
		 * This is the default. So, using this method is just to add documentation.
		 * Example: mock.expects("methodA).noArgs();
		 * 			mock.methodA();
		 * 			asserTrue(mock.errorMessage(), mock.sucess());
		 * 
		 * @return Returns the mock object. This makes it easy to string several
		 * 				method calls together.
		 * 
		 */		
		public function noArgs():Mock
		{
			return this;
		}
		

		/**
		 * Tells the mock to return an object when a method is called as expected.
		 *  
		 * @param returnValue	Specifies the value to return when a method is called as expected.
		 *
		 * Example: mock.expects("methodA").withArg(someParameter, anotherParam).willReturn(returnObject);
		 * 			var someObject:SomeType = mock.methodA(someParameter, anotherParam);
		 * 			asserTrue(mock.errorMessage(), mock.sucess());
		 */		
		public function willReturn(returnValue:Object):void
		{
			currentMethod.returnValue = returnValue;
		}
		
		/**
		 * Tells the mock to throw an error when called as expected.
		 *  
		 * @param exception	An Error object that will be thrown when the method is called.
		 * 
		 * Example: mock.expects("methodA").withArg(someParameter, anotherParam).willThrow(new Error("errorMessage"));
		 * 			try
		 * 			{
		 * 				mock.methodA(someParameter, anotherParam);
		 * 			}
		 * 			catch(error:Error)
		 * 			{
		 * 				asserEquals("errorMessage", error.message);
		 * 			}
		 * 			
		 * 
		 */		
		public function willThrow(exception:Object):void
		{
			currentMethod.exception = exception;
		}
		
		public function noReturn():void
		{
		}
		
		/**
		 * Compares, by value - not by reference, that expected arguments equal actual arguments. 
		 * @param value
		 * 
		 */		
		public function deepCompare(value:Boolean):void
		{
			currentMethod.deepCompare = value;
		}
		
		/**
		 * Tells the mock to record a method call as it is being called. This method is public so that 
		 * the mock can be used in composition, A test would rarely, if ever, call this method.
		 *  
		 * @param methodName	The name of the method that is being called.
		 * @param args			An array of the arguments being called.
		 * 
		 * Example: class SomeMockInComposition extends SomeClassOtherThanMock
		 * 			{
		 * 				var mock:Mock = new Mock();
		 * 				override public function methodA(someArg:SomeType):void
		 * 				{
		 * 					mock.record("methodA", someArg)l
		 * 				}
		 * 			}
		 * 
		 */		
		public function record(methodName:String, ...args):void
		{			
			var recordedMethod:RecordedMethod = new RecordedMethod(methodName,args);

			var index:int = lookupRecordedMethodInExpectedMethods(recordedMethod);
			if (index != -1)
			{
				var expectedMethod:MethodInvocation = expectedMethods[index];
				removeMethodCallFromExpectedList(index);
				currentReturnValue = expectedMethod.simulateMethod( args );
				return;
			} 
			
			if( _isLiberal ){
				currentReturnValue = null;
			} else {
				reason = "Was not expecting "+recordedMethod+" to be called.";
				testFailed = true;
			}
		}

		/**
		 * Checks to see if the mock was called as expected.
		 * This method is most often called from a test.
		 *  
		 * @return Returns true if the mock was called sucessfully, or false otherwise.
		 * 
		 * Example: mock.expects("methodA").withArg(someParameter, anotherParam);
		 * 								 mock.methodA(someParameter, anotherParam);
		 * 								 asserTrue(mock.errorMessage(), mock.sucess());
		 * 
		 */		
		public function success():Boolean
		{
			verify();
			return !testFailed;
		}

		/**
		 * If an object was not called the way it was expected, errorMessage() will return
		 * a string describing why.
		 *  
		 * @return Returns true if the mock was called sucessfully, or false otherwise.
		 * 
		 * Example: mock.expects("methodA").withArg(someParameter, anotherParam);
		 * 								 mock.methodA(someParameter, anotherParam);
		 * 								 asserTrue(mock.errorMessage(), mock.sucess());
		 * 
		 */	
		public function errorMessage():String
		{
			verify();
			return reason;
		}
		
		/**
		 * Tells the mock to record a method call as it is being called. This method is public so that 
		 * the mock can be used in composition, A test would rarely, if ever, call this method.
		 *  
		 * @param methodName	The name of the method that is being called.
		 * @param args			An array of the arguments being called.
		 * 
		 * Example: class SomeMockInComposition extends SomeClassOtherThanMock
		 * 			{
		 * 				var mock:Mock = new Mock();
		 * 				override public function methodA(someArg:TypeA):TypeB
		 * 				{
		 * 					mock.record("methodA", someArg);
		 * 					return expectedReturnFor() as TypeB;
		 * 				}
		 * 			}
		 * 
		 */	
		protected function expectedReturnFor(methodName:String="Depricated"):Object
		{
			return currentReturnValue;			
		}	
		
		
		private function removeMethodCallFromExpectedList(index:uint):void
		{
			if (expectedMethods[index].timesInvoked>1)
			{
				expectedMethods[index].timesInvoked--;
			} else {
				expectedMethods.splice(index, 1);
			}
		}
		
		private function lookupRecordedMethodInExpectedMethods(recordedMethod:RecordedMethod):int
		{
			for (var i:uint =0; i<=expectedMethods.length-1; i++)
			{
				var matchOutcome:Object = (expectedMethods[i] as MethodInvocation).matchesRecordedMethod(recordedMethod);
				if( matchOutcome.matched )
				{
					reason = matchOutcome.description;
					return i;
				}
			}
			return -1;			
		}
		

		/**
		 * This method is depricated. It is public for backwards compatability.
		 * It is used internally to run tests to see if all the expected methods have been
		 * called as expected.
		 * 
		 */		
		public function verify():void
		{	
			// Don't verify more than once to minimize processing
			if (hasBeenVerified) return;
			hasBeenVerified = true;
			if (expectedMethods.length>0)
			{
				testFailed=true;
				reason = getExpectedMethodCallNames(); 
				return;
			}
		}
		
		/**
		 * Depricated.
		 * 
		 * @return 
		 * 
		 */		
		public function hasError():Boolean
		{
			return !testFailed;
		}
		
		private function getExpectedMethodCallNames():String
		{
			var methodNamesString:String = "The following methods were expected but not called: \n";
			
			for (var i:uint=0; i<=expectedMethods.length-1; i++)
			{
				methodNamesString+=expectedMethods[i].toString()+" \n";
			}
			return methodNamesString;
		}
		
	}
}
	import flash.utils.ByteArray;
	import flash.utils.describeType;
	
	import org.mock4as.Mock;
	

class MethodInvocation 
{
   public function MethodInvocation(methodName : String)
   {
         name = methodName;
   }	
   
	public var name:String;
	public var timesInvoked:int = 1;
	public var args:Array = new Array();
	public var returnValue:Object;
	public var exception:Object;
	public var deepCompare:Boolean = false;
	public var closure:Function = null;
	
	public function expectAnyArgs():void{
		args = null;
	}
	public function get expectsAnyArgs():Boolean {
		return null == args;
	}
	
	public function matchesRecordedMethod( recordedMethod:RecordedMethod ):Object
	{	
		if (deepCompare)
		{
			var areEqual:Boolean = compare(args[i], recordedMethod.args[i]);
			if (!areEqual)
			{
				return{
						description: "Arrays do not have equal elements",
						matched: false
					};
			}
			return { description: null, matched: true }
		}
		if (name != recordedMethod.name)
			return { description: "expected "+name+" but was "+recordedMethod.name, matched: false }
		
		if (this.expectsAnyArgs)
			return { description: null, matched: true }

		
		if (args.length!=recordedMethod.args.length)
		{
			return{
				description: "Number of expected args does not equal number of actual args. Expected "+this+" but was "+recordedMethod,
				matched: false
			};
		}
		for (var i:uint=0; i<=recordedMethod.args.length-1; i++)
		{
			
			
			if (args[i] != recordedMethod.args[i]) 
			{
				return{
					description: "Expected "+this+" but was "+recordedMethod,
					matched: false
				};
			}
		}

		return { description: null, matched: true }
	}
	

	private function compare (obj1:Object, obj2:Object):Boolean 
	{
                       
            var b1:ByteArray = new ByteArray();
            var b2:ByteArray = new ByteArray();
           
            b1.writeObject(obj1);
            b2.writeObject(obj2);
           
            // compare the lengths first
            var size:uint = b1.length;
            if (b1.length == b2.length) {
                    b1.position = 0;
                    b2.position = 0;
                   
                    // then the bits
                    while (b1.position < size) {
                            var v1:int = b1.readByte();
                            if (v1 != b2.readByte()) {
                                    return false;
                            }
                    }                            
            }
           
           
            if (b1.toString() == b2.toString()) {
                    return true;
            }              
           
            return false;
    }
	
	public function simulateMethod( args:Array ):Object {
		if( null != closure )
			return closure.apply(null,args);
		else if( null != exception )
			throw exception;
		else
			return returnValue;
	}
	
	public function toString():String
	{
		return name+"("+args+")";
	}	
}

/**
 * Class used by the mock. Stores properties about a method that is expected to be called.
 * 
 */
class RecordedMethod
{
	public function RecordedMethod( methodName : String, methodArgs : Array )
	{
		name = methodName;
		args = methodArgs;
	}
	
	public function toString():String
	{
		return name+"("+args+")";
	}	
	
	public var name:String;
	public var args:Array;
}
    
